# -*- coding: utf-8 -*-
# -*- frozen_string_literal: true -*-

require "optparse"
require "optparse/uri"
require "forwardable"
require "erb"
require "fileutils"

# The script's main function.
def scaffold_gem
  options = ScaffoldCLI.new.parse $argv
  details = ScaffoldDetails.new options
  Scaffold.gem details
end

class Scaffold

  # Path character wildcard.
  WILDCARD = "*"

  # Global access to path, and its in-between directories.
  GLOB = "**"

  # Gem file full name.
  STAND_IN_NAME = "_gem_name"

  # Gem file name that may be scoped, like plugins.
  STAND_IN_BASENAME = "_gem_basename"

  # Default binstub alternative platform
  ALT_PLATFORM_ID = "-alpine"

  # Test file identifier
  TEST_IDENTIFIER = "test_"

  # Scaffold main entry point. Assumes __dir__ is the working directory.
  def self.gem details
    scaffold = new details, directory: __dir__
    scaffold.
      fill_templates.
      rename_templates.
      rename_gem_files.
      clean_up
  end

  def initialize details, parser: ERB, directory:, template_extension: ".erb"
    @scaffold = details
    @parser = parser
    @directory = set_scaffold_dir(directory)
    @template_extension = template_extension
  end

  def templates
    Dir[directory + GLOB + dir_div + WILDCARD + template_extension]
  end

  def fill template
    content = @parser.new(File.read template).result binding
    File.open(template, "wb") { |f| f.write content }
  end

  def fill_templates
    templates.each { |t| fill t }
    self
  end

  def rename_templates
    templates.each do |template|
      new_name = template.sub(%r/#{template_extension}\z/, "")
      FileUtils.mv template, new_name
    end
    self
  end

  def rename_gem_files
    path = directory + GLOB + dir_div + WILDCARD
    files =
      Dir[path + STAND_IN_NAME + WILDCARD] + Dir[path + STAND_IN_BASENAME + WILDCARD]

    files.map { |file| FileUtils.mv file, new_filename(file), force: true }
    self
  end

  def clean_up
    remove_binstub unless @scaffold.bin
    relocate_gem if @scaffold.plugin_namespace
    remove_scaffolding
  end

  private

  attr_reader :directory, :template_extension

  def new_filename file
    old_name = File.basename(file)
    new_name = old_name.include?(STAND_IN_NAME) ? @scaffold.gem_name : @scaffold.gem_basename
    new_name.prepend(TEST_IDENTIFIER) if old_name.start_with?(TEST_IDENTIFIER)
    new_name = new_name + ALT_PLATFORM_ID if old_name.end_with?(ALT_PLATFORM_ID)
    File.dirname(file) + dir_div + new_name + File.extname(file)
  end

  def remove_binstub
    %W[#{directory}/bin #{directory}/test/bin].each { |dir| FileUtils.rm_r dir }
  end

  def relocate_gem
    entry_point = "lib/#{@scaffold.main_basename}"

    FileUtils.mkdir_p "#{directory}#{entry_point}"
    FileUtils.mkdir_p "#{directory}test/#{entry_point}"

    FileUtils.mv Dir["#{directory}lib/**/*"],
      "#{directory}#{entry_point}", force: true

    FileUtils.mv Dir["#{directory}test/lib/**/*"],
      "#{directory}test/#{entry_point}", force: true
  end

  def remove_scaffolding
    %W[
      #{directory}.git
      #{directory}.scaffold-ci.yml
      #{directory}ReadMe.org
      #{directory}ChangeLog.org
      #{directory}scaffold_gem.rb
      #{directory}test/test_scaffold_gem.rb
    ].each { |sf| FileUtils.rm_rf sf }
  end

  def set_scaffold_dir dir
    dir + dir_div
  end

  def dir_div
    ScaffoldDetails::DIR_DIV
  end
end

class ScaffoldDetails

  CAMEL_CASE_REGEX = /([a-z\d])([A-Z])/
  CONSTANT_RESOLUTION_OPERATOR = File::PATH_SEPARATOR * 2
  PLUGIN_IDENTIFIER = "-"
  DIR_DIV = File::SEPARATOR
  DIR_BACKTRACK = ".."

  extend Forwardable

  def_delegators :@options, :namespace, :author, :email, :repo, :license, :bin
  public :binding

  def initialize options
    @options = options
    @formatted_name = options.formatted_name
  end

  # Gem main namespace #=> SomeGem
  def main_namespace
    split_namespace.first
  end

  # Gem plugin namespace #=> SomePlugin
  def plugin_namespace
    _, scoped, _ = split_namespace
    scoped
  rescue
    nil
  end

  # The gem's namespace
  # Given SomeGem::SomePlugin #=> SomePlugin
  # Given SomeGem #=> SomeGem
  def gem_namespace
    plugin_namespace or main_namespace
  end

  # The gem's name as register in Rubygems.org
  # Given SomeGem::SomePlugin #=> some_gem-some_plugin
  def gem_name
    snake_case = '\1_\2'
    @gem_name ||= namespace.
      gsub(CAMEL_CASE_REGEX, snake_case).
      downcase.
      gsub CONSTANT_RESOLUTION_OPERATOR, PLUGIN_IDENTIFIER
  end

  # The gem's constant
  # Given SomeGem #=> SOME_GEM
  # Given SomeGem::SomePlugin #=> SOME_PLUGIN
  def gem_constant
    @constant ||= begin
                    main, plugin, _ = gem_name_parts.map &:upcase
                    plugin or main
                  end
  end

  # Gem formatted name. As display in the ReadMe.
  # Given SomeGem::SomePlugin #=> Some Gem - Some Plugin
  # Can be setup to any valid string via the options given to a new scaffold_details.
  def formatted_name
    return @formatted_name unless @formatted_name.empty?
    words = '\1 \2'
    @formatted_name = namespace.
      gsub(CAMEL_CASE_REGEX, words).
      gsub CONSTANT_RESOLUTION_OPERATOR, formatted_plugin_identifier
  end

  # Gem main basename
  # Whether given SomeGem or SomeGem::SomePlugin #=> some_gem
  def main_basename
    gem_name_parts[0]
  end

  # Gem plugin basename
  # Given SomeGem::SomePlugin #=> some_plugin
  def plugin_basename
    gem_name_parts[1]
  end

  # Gem's basename
  # Given SomeGem::SomePlugin #=> some_plugin
  # Given SomeGem #=> some_gem
  def gem_basename
    plugin_basename or main_basename
  end

  # Gem's path
  # Given SomeGem #=> some_gem
  # Given SomeGem::SomePlugin #=> some_gem/some_plugin
  def gem_path
    plugin = DIR_DIV + plugin_basename if plugin_basename
    "#{main_basename}#{plugin}"
  end

  # Print the gem's require statement
  # Whether given SomeGem or SomeGem::SomePlugin #=> require "some_gem"
  def require_main
    %Q{require "#{main_basename}"}
  end

  # Print the gem's require statement as a plugin
  # Given SomeGem::SomePlugin #=> require "some_gem/some_plugin"
  def require_plugin
    return unless plugin_basename
    %Q{require "#{gem_path}"}
  end

  # Print minitest's config helper require statement from toplevel files
  # Given SomeGem #=> require_relative "./../_config/minitest"
  # Given SomeGem::SomePlugin #=> require_relative "./../../_config/minitest"
  def toplevel_require_minitest_config
    backtrack = plugin_namespace ? double_backtrack_dir : DIR_BACKTRACK
    require_minitest_config backtrack
  end

  # Print minitest's config helper require statement from sublevel files
  # Given SomeGem #=> require_relative "./../../_config/minitest"
  # Given SomeGem::SomePlugin #=> require_relative "./../../../_config/minitest"
  def sublevel_require_minitest_config
    triple_backtrack = double_backtrack_dir + backtrack_again
    backtrack = plugin_namespace ? triple_backtrack : double_backtrack_dir
    require_minitest_config backtrack
  end

  private

  def double_backtrack_dir
    DIR_BACKTRACK + backtrack_again
  end

  def backtrack_again
    DIR_DIV + DIR_BACKTRACK
  end

  def require_minitest_config backtrack
    %Q{require_relative "./#{backtrack}/_config/minitest"}
  end

  def gem_name_parts
    @gem_name_parts ||= gem_name.split PLUGIN_IDENTIFIER
  end

  def formatted_plugin_identifier
    " #{PLUGIN_IDENTIFIER} "
  end

  def split_namespace
    namespace.split CONSTANT_RESOLUTION_OPERATOR
  end
end

class ScaffoldCLI
  # The scaffold CLI options. Can be initialized using keywords.
  # Missing members default to nil, except for bin, falling back to +false+, and
  # formatted_name, to an empty string.
  ScaffoldOptions = Struct.new :namespace, :author, :email, :repo, :license,
    :bin, :formatted_name, keyword_init: true do

    def initialize namespace:nil, author:nil, email:nil, repo:nil, license:nil,
        bin:nil, formatted_name:nil
      super
      self.bin ||= false
      self.formatted_name ||= ""
    end
  end

  def initialize options=ScaffoldOptions.new, notice=$stdout, err=$stderr
    @notice = notice
    @options = options
    @err = err
  end

  def parse argv=$argv, parser: OptionParser
    cli = build_argv_parser parser
    cli.parse!(*argv)
    check_mandatory_values options, cli
    options.freeze
  end

  private # :nodoc:

  attr_reader :options, :notice, :err

  def check_mandatory_values opts, cli
    all_mandatory_values =
      opts.namespace && opts.author && opts.email && opts.repo && opts.license

    unless all_mandatory_values
      err.puts "Must provide all mandatory options."
      show_help cli
      exit_cli status: 2
    end
  end

  def build_argv_parser parser
    parser.new do |p|
      set_formatted_name p
      set_bin p
      set_license p
      set_repo p
      set_email p
      set_author p
      set_namespace p
      display_help p
    end
  end

  def set_formatted_name cli
    desc = "Add unsupported name style convention"
    cli.on("-f", "--formatted-name=NAME", String, desc) { |n| options.formatted_name = n }
  end

  def set_bin cli
    desc = "Add binstub"
    cli.on("-b", "--bin", TrueClass, desc) { |b| options.bin = b }
  end

  def set_license cli
    desc = "Set mandatory license identifier. Details https://spdx.org/licenses"
    cli.on("-l", "--license=LICENSE", String, desc) { |l| options.license = l }
  end

  def set_repo cli
    desc = "Set mandatory gem repo URL"
    cli.on("-r", "--repo=REPO", URI, desc) { |r| options.repo = r.to_s }
  end

  def set_email cli
    desc = "Set mandatory gem contact email"
    # regex from https://www.owasp.org/index.php/OWASP_Validation_Regex_Repository
    regex = /\A[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}\z/
    cli.on("-e", "--email=EMAIL", regex, desc) { |e| options.email = e }
  end

  def set_author cli
    desc = "Set mandatory gem author"
    cli.on("-a", "--author=AUTHOR", String, desc) { |a| options.author = a }
  end

  def set_namespace cli
    desc = "Set mandatory gem namespace"
    cli.on("-n", "--namespace=NAMESPACE", String, desc) { |n| options.namespace = n }
  end

  def display_help cli
    cli.on_tail("-h", "--help", "Show this message") {
      show_help cli
      exit_cli
    }
  end

  def show_help cli
    notice.puts cli
  end

  def exit_cli status: 0
    exit status unless ENV["GEM_SCAFFOLD_ENV"] == "test"
  end
end

# The ruby interpreter scaffold_gem when the script is run as command line program
if $PROGRAM_NAME == __FILE__
  scaffold_gem
end
